package com.zoro.barn.business.service.sqldocument.business.sql.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.zoro.barn.commons.db.enums.Order;
import com.zoro.barn.business.service.sqldocument.business.sql.dao.BarnSqlHistoryMapper;
import com.zoro.barn.business.service.sqldocument.business.sql.dao.BarnSqlInfoMapper;
import com.zoro.barn.business.service.sqldocument.business.sql.dao.BarnSqlUsageMapper;
import com.zoro.barn.business.service.sqldocument.business.sql.dto.SqlMoveLockUserDto;
import com.zoro.barn.business.service.sqldocument.business.sql.entity.BarnSqlHistory;
import com.zoro.barn.business.service.sqldocument.business.sql.entity.BarnSqlInfo;
import com.zoro.barn.business.service.sqldocument.business.sql.entity.BarnSqlUsage;
import com.zoro.barn.business.service.sqldocument.business.sql.errors.MoveSqlBusyException;
import com.zoro.barn.business.service.sqldocument.business.sql.errors.ProductDontSameException;
import com.zoro.barn.commons.mongodb.dao.MongoDao;
import com.zoro.barn.commons.redis.tools.RedisOperation;
import com.zoro.barn.commons.tools.encryption.EncryptUtils;
import com.zoro.barn.commons.tools.json.JsonUtil;
import com.zoro.barn.commons.tools.string.IDs;
import com.zoro.barn.commons.foundation.exceptions.DataIncompleteException;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import jakarta.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author zhaoxingwu
 */
@Service
public class BarnSqlInfoService {

    private static final String SQL_ID_KEY = "sqlId";

    @Resource
    private BarnSqlInfoMapper barnSqlInfoMapper;
    @Resource
    private BarnSqlUsageMapper barnSqlUsageMapper;
    @Resource
    private BarnSqlHistoryMapper barnSqlHistoryMapper;

    private final MongoDao mongoDao;
    private final RedisOperation redisOperation;

    @Value("${mongo.file.db-name:sql}")
    private String dbName;

    public BarnSqlInfoService(MongoDao mongoDao, RedisOperation redisOperation) {
        this.mongoDao = mongoDao;
        this.redisOperation = redisOperation;
    }


    /**
     * 条件查询
     *
     * @param barnSqlInfo 查询条件
     * @param order       sqlOrder 是倒序还是正序
     * @return result
     */
    public List<BarnSqlInfo> query(BarnSqlInfo barnSqlInfo, Order order) {
        return this.barnSqlInfoMapper.query(barnSqlInfo, order);
    }

    public List<BarnSqlInfo> query(BarnSqlInfo barnSqlInfo) {
        return this.query(barnSqlInfo, Order.DESC);
    }


    @Transactional(rollbackFor = Exception.class)
    public BarnSqlInfo insert(BarnSqlInfo barnSqlInfo, String sqlText, List<BarnSqlUsage> use) {

        if (StringUtils.isBlank(barnSqlInfo.getCreateId())) {
            throw new DataIncompleteException("create id can not null");
        }
        if (StringUtils.isBlank(sqlText)) {
            throw new DataIncompleteException("SQL can not  empty");
        }
        if (StringUtils.isBlank(barnSqlInfo.getSqlTitle())) {
            throw new DataIncompleteException("SQL title can not  empty");
        }

        // insert mongo
        barnSqlInfo.setPid(IDs.uuid());
        barnSqlInfo.setCreateTime(new Date());

        String documentId = this.uploadSql(sqlText, barnSqlInfo.getPid(), barnSqlInfo.getSqlTitle());
        barnSqlInfo.setDocumentId(documentId);

        // 这里应该查询 最大 order 是多少
        int order = this.barnSqlInfoMapper.maxOrder(barnSqlInfo.getProductId());
        barnSqlInfo.setSqlOrder(order + 1);
        // insert usage
        this.barnSqlInfoMapper.insert(barnSqlInfo);
        Collection<BarnSqlUsage> data = this.resolveUseData(use, barnSqlInfo.getPid());
        if (CollectionUtils.isNotEmpty(data) && StringUtils.isNotBlank(barnSqlInfo.getProductId())) {
            this.barnSqlUsageMapper.insertBatch(data);
        }

        return barnSqlInfo;
    }

    @Transactional(rollbackFor = Exception.class)
    public BarnSqlInfo update(BarnSqlInfo barnSqlInfo, String sqlText, List<BarnSqlUsage> use) {

        if (StringUtils.isAnyBlank(barnSqlInfo.getPid(), barnSqlInfo.getUpdateId())) {
            throw new DataIncompleteException("pid or update id can not be null!");
        }

        BarnSqlInfo ori = this.barnSqlInfoMapper.selectById(barnSqlInfo.getPid());
        if (ori == null) {
            throw new NullPointerException("lost original data : " + barnSqlInfo.getPid());
        }

        // save history
        Date now = new Date();
        BarnSqlHistory history = new BarnSqlHistory();
        history.setPid(IDs.uuid());
        history.setSqlId(ori.getPid());
        history.setDocumentId(ori.getDocumentId());
        history.setUpdateId(barnSqlInfo.getUpdateId());
        history.setUpdateTime(now);

        this.barnSqlHistoryMapper.insert(history);

        // update mongo db
        String sha = EncryptUtils.sha256(sqlText);
        String same = sameSql(barnSqlInfo.getPid(), sha);

        if (StringUtils.isBlank(same)) {
            String documentId = this.uploadSql(sqlText, barnSqlInfo.getPid(), barnSqlInfo.getSqlTitle());
            barnSqlInfo.setDocumentId(documentId);
        } else {
            barnSqlInfo.setDocumentId(same);
        }
        barnSqlInfo.setUpdateTime(now);
        if (!StringUtils.equals(ori.getProductId(), barnSqlInfo.getProductId()) && StringUtils.isNotBlank(barnSqlInfo.getProductId())) {

            int newOrder = this.barnSqlInfoMapper.maxOrder(barnSqlInfo.getProductId());
            barnSqlInfo.setSqlOrder(newOrder + 1);
            List<BarnSqlInfo> list = new ArrayList<>();
            list.add(barnSqlInfo);
            this.barnSqlInfoMapper.updateOrderBatch(list);
        }
        this.barnSqlInfoMapper.update(barnSqlInfo);

        // 删除旧的，插入新的
        this.barnSqlUsageMapper.deleteBySqlId(barnSqlInfo.getPid());
        Collection<BarnSqlUsage> data = this.resolveUseData(use, barnSqlInfo.getPid());
        if (CollectionUtils.isNotEmpty(data) && StringUtils.isNotBlank(barnSqlInfo.getProductId())) {
            this.barnSqlUsageMapper.insertBatch(data);
        }
        return barnSqlInfo;
    }

    /**
     * 1.remove the same data
     * 2.set pid and sql id
     *
     * @param use   data
     * @param sqlId sql id
     * @return resoled data
     */
    private Collection<BarnSqlUsage> resolveUseData(Collection<BarnSqlUsage> use, String sqlId) {
        if (CollectionUtils.isEmpty(use)) {
            return null;
        }
        use.forEach(v -> {
            v.setPid(IDs.uuid());
            v.setSqlId(sqlId);
        });
        return use.stream().collect(Collectors.toMap(BarnSqlUsage::getProjectId, v -> v, (a, b) -> b)).values();
    }

    /**
     * 上传到 mongo
     *
     * @param sqlText  sql 文
     * @param sqlId    sql info id
     * @param sqlTitle sql name
     * @return document id
     */
    private String uploadSql(String sqlText, String sqlId, String sqlTitle) {
        ByteArrayInputStream is = new ByteArrayInputStream(sqlText.getBytes(StandardCharsets.UTF_8));
        Map<String, Object> meta = new HashMap<>();
        meta.put(SQL_ID_KEY, sqlId);
        String sha = EncryptUtils.sha256(sqlText);
        return this.mongoDao.upload(this.dbName, is, sqlTitle + ".sql", meta, sha);
    }

    private String sameSql(String sqlId, String sha) {
        Map<String, Object> param = new HashMap<>();
        param.put(SQL_ID_KEY, sqlId);
        return this.mongoDao.findSameFile(this.dbName, sha, param);
    }

    /**
     * delete one sql and it's related use ,history and files
     *
     * @param pid pid
     */
    public void deleteById(String pid) {
        if (StringUtils.isBlank(pid)) {
            return;
        }

        BarnSqlInfo info = barnSqlInfoMapper.selectById(pid);
        if (info == null) {
            return;
        }
        List<BarnSqlHistory> hi = barnSqlHistoryMapper.getBySqlId(pid);

        // delete history that stored in mongo
        Set<String> fileIds = new HashSet<>();
        fileIds.add(info.getDocumentId());
        if (CollectionUtils.isNotEmpty(hi)) {
            hi.forEach(v -> {
                if (StringUtils.isNotBlank(v.getDocumentId())) {
                    fileIds.add(v.getDocumentId());
                }
            });
        }
        fileIds.forEach(v -> this.mongoDao.deleteById(this.dbName, v));

        this.barnSqlInfoMapper.deleteById(pid);
        // delete history
        this.barnSqlHistoryMapper.deleteBySqlId(pid);
        // delete usage
        this.barnSqlUsageMapper.deleteBySqlId(pid);
    }

    /**
     * 查询一个sql 的基本信息
     *
     * @param pid pid
     * @return sql info
     */
    public BarnSqlInfo getById(String pid) {
        if (StringUtils.isBlank(pid)) {
            return null;
        }
        return this.barnSqlInfoMapper.selectById(pid);

    }

    /**
     * 查询 sql 正文
     *
     * @param pid sql info id
     * @return sql text
     */
    public String getSqlText(String pid) {
        if (StringUtils.isBlank(pid)) {
            return null;
        }

        ByteArrayOutputStream os = this.mongoDao.download(this.dbName, pid);
        return os.toString();
    }

    /**
     * 查找sql 使用情况
     *
     * @param sqlId sql info id
     * @return who use
     */
    public List<BarnSqlUsage> getUseBySqlId(String sqlId) {
        return this.barnSqlUsageMapper.getBySqlId(sqlId);
    }

    /**
     * put one sql to another sql's position ,this method only allow one user invoke at the same time .
     * because the second user's data must be wrong, so the result must wrong
     *
     * @param from        sql id that is going to move
     * @param to          sql id you want to put the FROM SQL
     * @param userName    who is doing this
     * @param userAccount who is doing this
     * @throws JsonProcessingException e
     */
    @Transactional(rollbackFor = Exception.class)
    public void moveSingleSql(String from, String to, String userName, String userAccount) throws JsonProcessingException {

        if (StringUtils.equals(from, to)) {
            return;
        }

        if (StringUtils.isAnyBlank(from, to)) {
            throw new DataIncompleteException("参数错误: from:" + from + ";to:" + to);
        }
        BarnSqlInfo fromSql = this.getById(from);
        BarnSqlInfo toSql = this.getById(to);

        if (Objects.equals(fromSql.getSqlOrder(), toSql.getSqlOrder())) {
            return;
        }
        if (fromSql.getSqlOrder() == null || toSql.getSqlOrder() == null) {
            throw new DataIncompleteException("sql 数据不全，重新更新 sql 后重试");
        }

        if (StringUtils.isAnyBlank(fromSql.getProductId(), toSql.getProductId())) {
            throw new DataIncompleteException("数据丢失，没有找到sql对应的产品数据");
        }

        if (!StringUtils.equals(fromSql.getProductId(), toSql.getProductId())) {
            throw new ProductDontSameException();
        }

        SqlMoveLockUserDto user = new SqlMoveLockUserDto();
        user.setUserAccount(userAccount);
        user.setUserName(userName);
        String value = JsonUtil.toString(user);

        // 先加个锁
        // 将锁的等等设置为0 因为等其他人操作完成，另一个用户继续操作是错的，因为用户在界面上看到的数据已经是旧的，这时候应该刷新数据再确认操作
        String lockId = "MOVE_SINGLE_SQL::" + fromSql.getProductId();
        String whoDoing = this.redisOperation.lock(lockId, value, Duration.ofSeconds(60), Duration.ofSeconds(0));
        if (whoDoing == null) {
            // 然后开始移动
            try {
                 /*
                 1. 查出区间内的所有sql ,不包含这两条
                 2. 计算是向前移动还是向后移动 ，
                    向前：区间内的sql 和 TO-SQL 的order 都+1, 令FROM-SQL的 order 等于 TO-SQL的值 ，如果区间内没有，更新 from 和 to 就可
                    向后：区间内的sql 的 order 都 -1 ，TO-SQL 不动，令FROM-SQL的值等于 最后一个区间内sql的值，如果没有区间内没有，更新，什么都不做
                 3. update
                 */
                List<BarnSqlInfo> range;
                if (fromSql.getSqlOrder() > toSql.getSqlOrder()) {
                    // 向前
                    range = this.barnSqlInfoMapper.sqlRange(fromSql.getProductId(), toSql.getSqlOrder(), fromSql.getSqlOrder());
                    if (CollectionUtils.isEmpty(range)) {
                        range = new ArrayList<>();
                        int tmp = fromSql.getSqlOrder();
                        fromSql.setSqlOrder(toSql.getSqlOrder());
                        toSql.setSqlOrder(tmp);
                        range.add(fromSql);
                        range.add(toSql);
                    } else {
                        int tmp = toSql.getSqlOrder();
                        fromSql.setSqlOrder(tmp);
                        range.add(0, toSql);
                        range.forEach(v -> v.setSqlOrder(v.getSqlOrder() + 1));
                        range.add(0, fromSql);
                    }
                } else {
                    // 向后
                    range = this.barnSqlInfoMapper.sqlRange(fromSql.getProductId(), fromSql.getSqlOrder(), toSql.getSqlOrder());
                    if (!CollectionUtils.isEmpty(range)) {
                        int last = range.get(range.size() - 1).getSqlOrder();
                        fromSql.setSqlOrder(last);
                        range.forEach(v -> v.setSqlOrder(v.getSqlOrder() - 1));
                        range.add(fromSql);
                    }
                }
                if (!CollectionUtils.isEmpty(range)) {
                    // 更新
                    // todo-future in this point ,if the "range" is too large ,it will throw a exception
                    this.barnSqlInfoMapper.updateOrderBatch(range);
                }
            } finally {
                // 移动之后 解开锁
                this.redisOperation.unlock(lockId);
            }
        } else {
            SqlMoveLockUserDto who = JsonUtil.toJavaType(whoDoing, SqlMoveLockUserDto.class);
            if (who == null) {
                throw MoveSqlBusyException.build(null, null);
            } else {
                throw MoveSqlBusyException.build(who.getUserAccount(), who.getUserName());
            }
        }
    }

    /**
     * 查询 一个 product 的sql,不包含 被特定 project 使用的项目
     *
     * @param product product id
     * @param start   sql order
     * @param end     sql order
     * @return sql info set
     */
    public List<BarnSqlInfo> getProductSql(String product, Integer start, Integer end) {
        return this.barnSqlInfoMapper.getProductSql(product, start, end);
    }

    /**
     * 查询一个 product 的sql ， 包含 指定project
     *
     * @param product product id
     * @param project project id
     * @param start   start sql order
     * @param end     end sql order
     * @return sql info set
     */
    public List<BarnSqlInfo> getProjectSql(String product, String project, Integer start, Integer end) {
        return this.barnSqlInfoMapper.getProjectSql(product, project, start, end);
    }


}
